﻿// Sections of this code have been abridged from https://github.com/mono/mono/blob/master/mcs/class/corlib/System/TermInfoReader.cs under the MIT license

using System;
using System.IO;
using System.Linq;
using System.Text;

namespace BepInEx.Unix;

internal class TtyInfo
{
    public string TerminalType { get; set; } = "default";

    public int MaxColors { get; set; }

    public string[] ForegroundColorStrings { get; set; }

    public static TtyInfo Default { get; } = new()
    {
        MaxColors = 0
    };

    public string GetAnsiCode(ConsoleColor color)
    {
        if (MaxColors <= 0 || ForegroundColorStrings == null)
            return string.Empty;

        var index = (int) color % MaxColors;
        return ForegroundColorStrings[index];
    }
}

internal static class TtyHandler
{
    private static readonly string[] ncursesLocations =
    {
        "/usr/share/terminfo",
        "/etc/terminfo",
        "/usr/lib/terminfo",
        "/lib/terminfo"
    };

    private static string TryTermInfoDir(string dir, string term)
    {
        var infoFilePath = $"{dir}/{(int) term[0]:x}/{term}";

        if (File.Exists(infoFilePath))
            return infoFilePath;

        infoFilePath = Utility.CombinePaths(dir, term.Substring(0, 1), term);

        if (File.Exists(infoFilePath))
            return infoFilePath;

        return null;
    }

    private static string FindTermInfoPath(string term)
    {
        if (string.IsNullOrEmpty(term))
            return null;

        var termInfoVar = Environment.GetEnvironmentVariable("TERMINFO");
        if (termInfoVar != null && Directory.Exists(termInfoVar))
        {
            var text = TryTermInfoDir(termInfoVar, term);
            if (text != null) return text;
        }

        foreach (var location in ncursesLocations)
            if (Directory.Exists(location))
            {
                var text = TryTermInfoDir(location, term);

                if (text != null)
                    return text;
            }

        return null;
    }

    public static TtyInfo GetTtyInfo(string terminal = null)
    {
        terminal = terminal ?? Environment.GetEnvironmentVariable("TERM");
        var path = FindTermInfoPath(terminal);

        if (path == null)
            return TtyInfo.Default;

        var buffer = File.ReadAllBytes(path);

        var info = TtyInfoParser.Parse(buffer);
        info.TerminalType = terminal;

        return info;
    }
}

internal static class TtyInfoParser
{
    private static readonly int[] ansiColorMapping =
    {
        0, 4, 2, 6, 1, 5, 3, 7, 8, 12, 10, 14, 9, 13, 11, 15
    };

    public static TtyInfo Parse(byte[] buffer)
    {
        int intSize;


        int magic = GetInt16(buffer, 0);

        switch (magic)
        {
            case 0x11a:
                intSize = 2;
                break;

            case 0x21E:
                intSize = 4;
                break;

            default:
                // Unknown ttyinfo format
                return TtyInfo.Default;
        }

        int boolFieldLength = GetInt16(buffer, 4);
        int intFieldLength = GetInt16(buffer, 6);
        int strOffsetFieldLength = GetInt16(buffer, 8);

        // Normally i'd put a more complete implementation here, but I only need to parse this info to get the max color count
        // Feel free to implement the rest of this using these sources:
        // https://github.com/mono/mono/blob/master/mcs/class/corlib/System/TermInfoReader.cs
        // https://invisible-island.net/ncurses/man/term.5.html
        // https://invisible-island.net/ncurses/man/terminfo.5.html

        var baseOffset = 12 + GetString(buffer, 12).Length + 1; // Skip the terminal name
        baseOffset += boolFieldLength;                          // Length of bool field section
        baseOffset += baseOffset % 2;                           // Correct for boundary

        var colorOffset =
            baseOffset
          + intSize * (int) TermInfoNumbers.MaxColors; // Finally the offset for the max color integer

        //int stringOffset = baseOffset + (intSize * intFieldLength);

        //int foregoundColorOffset =
        //	stringOffset
        //	+ (2 * (int)TermInfoStrings.SetAForeground);

        //foregoundColorOffset = stringOffset
        //					   + (2 * strOffsetFieldLength)
        //					   + GetInt16(buffer, foregoundColorOffset);

        var info = new TtyInfo();

        info.MaxColors = GetInteger(intSize, buffer, colorOffset);

        //string setForegroundTemplate = GetString(buffer, foregoundColorOffset);

        //info.ForegroundColorStrings = ansiColorMapping.Select(x => setForegroundTemplate.Replace("%p1%", x.ToString())).ToArray();
        info.ForegroundColorStrings =
            ansiColorMapping.Select(x => $"\u001B[{(x > 7 ? 82 + x : 30 + x)}m").ToArray();

        return info;
    }

    private static int GetInt32(byte[] buffer, int offset) =>
        buffer[offset]
      | (buffer[offset + 1] << 8)
      | (buffer[offset + 2] << 16)
      | (buffer[offset + 3] << 24);

    private static short GetInt16(byte[] buffer, int offset) =>
        (short) (buffer[offset]
               | (buffer[offset + 1] << 8));

    private static int GetInteger(int intSize, byte[] buffer, int offset) =>
        intSize == 2
            ? GetInt16(buffer, offset)
            : GetInt32(buffer, offset);

    private static string GetString(byte[] buffer, int offset)
    {
        var length = 0;

        while (buffer[offset + length] != 0x00)
            length++;

        return Encoding.ASCII.GetString(buffer, offset, length);
    }

    internal enum TermInfoNumbers
    {
        MaxColors = 13
    }

    internal enum TermInfoStrings
    {
        SetAForeground = 359
    }
}
